import Ajv from "ajv";
const i18n = require("ajv-i18n"); // eslint-disable-line
import { toPath } from "lodash";
import { Schema } from "./types";
import { isObject } from "./utils";
interface TransformErrorsObject {
  name: string;
  property: string;
  message: string | undefined;
  params: Ajv.ErrorParameters;
  schemaPath: string;
}
export type ErrorSchema = {
  [level: string]: ErrorSchema;
} & {
  __errors?: string[];
};

function toErrorSchema(errors: TransformErrorsObject[]) {
  if (errors.length < 1) return {};
  return errors.reduce((errorSchema, error) => {
    const { property, message } = error;
    const path = toPath(property); // /obj/a   -> [obj,a]
    let parent = errorSchema;

    if (path.length > 0 && path[0] === "") {
      path.splice(0, 1);
    }

    for (const segment of path.slice(0)) {
      if (!(segment in parent)) {
        (parent as any)[segment] = {};
      }
      parent = parent[segment];
    }

    if (Array.isArray(parent.__errors)) {
      parent.__errors = parent.__errors.concat(message || "");
    } else {
      if (message) {
        parent.__errors = [message];
      }
    }
    return errorSchema;
  }, {} as ErrorSchema);
}

function transformErrors(
  errors: Ajv.ErrorObject[] | null | undefined
): TransformErrorsObject[] {
  if (errors === null || errors === undefined) return [];
  return errors.map(({ message, dataPath, keyword, params, schemaPath }) => {
    return {
      name: keyword,
      property: `${dataPath}`,
      message,
      params,
      schemaPath,
    };
  });
}

export async function validateFormData(
  validator: Ajv.Ajv,
  formData: any,
  schema: Schema,
  locale = "zh",
  customValidate?: (data: any, errors: any) => void
) {
  let validationError: any = null;
  try {
    validator.validate(schema, formData);
  } catch (error) {
    validationError = error;
  }
  i18n[locale](validator.errors);

  let errors = transformErrors(validator.errors);
  if (validationError) {
    errors = [
      ...errors,
      {
        message: validationError.message,
      } as TransformErrorsObject,
    ];
  }

  const errorSchema = toErrorSchema(errors);

  if (!customValidate) {
    return {
      errors,
      errorSchema,
      valid: errors.length === 0,
    };
  }
  /**
   * {
   *      obj:{
   *        a:{b:str},
   *        _errors:[]
   *      }
   * }
   *
   *
   * row.obj.a
   */
  const proxy = createErrorProxy();
  await customValidate(formData, proxy);
  const newErrorSchema = mergeObjects(errorSchema, proxy, true);
  return {
    errors,
    errorSchema: newErrorSchema,
    valid: errors.length === 0,
  };
}

function createErrorProxy() {
  const raw = {};
  return new Proxy(raw, {
    get(target, key, reciver) {
      if (key === "addError") {
        return (msg: string) => {
          const __errors = Reflect.get(target, "__errors", reciver);
          if (__errors && Array.isArray(__errors)) {
            __errors.push(msg);
          } else {
            (target as any).__errors = [msg];
          }
        };
      }
      const res = Reflect.get(target, key, reciver);
      if (res === undefined) {
        const p: any = createErrorProxy();
        (target as any)[key] = p;
        return p;
      }
      return res;
    },
  });
}

export function mergeObjects(obj1: any, obj2: any, concatArrays = false) {
  const acc = Object.assign({}, obj1);
  return Object.keys(obj2).reduce((acc, key) => {
    const left = obj1 ? obj1[key] : {},
      right = obj2[key];
    if (obj1 && obj1.hasOwnProperty(key) && isObject(right)) {
      acc[key] = mergeObjects(left, right, concatArrays);
    } else if (concatArrays && Array.isArray(left) && Array.isArray(right)) {
      acc[key] = left.concat(right);
    } else {
      acc[key] = right;
    }
    return acc;
  }, acc);
}
